def create_message(self, describe=False): """ Creates a basic empty Message object with basic boilerplate metadata :return: Response object with execution information and the new message object inside the data envelope :rtype: Response """ # Internal documentation setup #allowable_parameters = { 'action': { 'None' } } allowable_parameters = { 'dsl_command': '`create_message()`' } # can't get this name at run-time, need to manually put it in per https://www.python.org/dev/peps/pep-3130/ if describe: allowable_parameters[ 'brief_description'] = """The `create_message` method creates a basic empty Message object with basic boilerplate metadata such as reasoner_id, schema_version, etc. filled in. This DSL command takes no arguments""" return allowable_parameters #### Define a default response response = Response() self.response = response #### Create the top-level message response.info("Creating an empty template ARAX Message") message = Message() self.message = message #### Fill it with default information message.id = None message.type = "translator_reasoner_message" message.reasoner_id = "ARAX" message.tool_version = RTXConfiguration().version message.schema_version = "0.9.3" message.message_code = "OK" message.code_description = "Created empty template Message" message.context = "https://raw.githubusercontent.com/biolink/biolink-model/master/context.jsonld" #### Why is this _datetime ?? FIXME message._datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S") #### Create an empty master knowledge graph message.knowledge_graph = KnowledgeGraph() message.knowledge_graph.nodes = [] message.knowledge_graph.edges = [] #### Create an empty query graph message.query_graph = QueryGraph() message.query_graph.nodes = [] message.query_graph.edges = [] #### Create empty results message.results = [] message.n_results = 0 #### Return the response response.data['message'] = message return response
def _expand_node(self, qnode_id: str, kp_to_use: str, continue_if_no_results: bool, query_graph: QueryGraph, use_synonyms: bool, synonym_handling: str, log: Response) -> DictKnowledgeGraph: # This function expands a single node using the specified knowledge provider log.debug(f"Expanding node {qnode_id} using {kp_to_use}") query_node = eu.get_query_node(query_graph, qnode_id) answer_kg = DictKnowledgeGraph() if log.status != 'OK': return answer_kg if not query_node.curie: log.error( f"Cannot expand a single query node if it doesn't have a curie", error_code="InvalidQuery") return answer_kg copy_of_qnode = eu.copy_qnode(query_node) if use_synonyms: self._add_curie_synonyms_to_query_nodes(qnodes=[copy_of_qnode], log=log, kp=kp_to_use) if copy_of_qnode.type in ["protein", "gene"]: copy_of_qnode.type = ["protein", "gene"] log.debug(f"Modified query node is: {copy_of_qnode.to_dict()}") # Answer the query using the proper KP valid_kps_for_single_node_queries = ["ARAX/KG1", "ARAX/KG2"] if kp_to_use in valid_kps_for_single_node_queries: from Expand.kg_querier import KGQuerier kg_querier = KGQuerier(log, kp_to_use) answer_kg = kg_querier.answer_single_node_query(copy_of_qnode) log.info( f"Query for node {copy_of_qnode.id} returned results ({eu.get_printable_counts_by_qg_id(answer_kg)})" ) # Make sure all qnodes have been fulfilled (unless we're continuing if no results) if log.status == 'OK' and not continue_if_no_results: if copy_of_qnode.id not in answer_kg.nodes_by_qg_id or not answer_kg.nodes_by_qg_id[ copy_of_qnode.id]: log.error( f"Returned answer KG does not contain any results for QNode {copy_of_qnode.id}", error_code="UnfulfilledQGID") return answer_kg if synonym_handling != 'add_all': answer_kg, edge_node_usage_map = self._deduplicate_nodes( dict_kg=answer_kg, edge_to_nodes_map={}, log=log) return answer_kg else: log.error( f"Invalid knowledge provider: {kp_to_use}. Valid options for single-node queries are " f"{', '.join(valid_kps_for_single_node_queries)}", error_code="InvalidKP") return answer_kg
def _answer_one_hop_query_using_neo4j(self, cypher_query: str, qedge_id: str, kp: str, continue_if_no_results: bool, log: Response) -> List[Dict[str, List[Dict[str, any]]]]: log.info(f"Sending cypher query for edge {qedge_id} to {kp} neo4j") results_from_neo4j = self._run_cypher_query(cypher_query, kp, log) if log.status == 'OK': columns_with_lengths = dict() for column in results_from_neo4j[0]: columns_with_lengths[column] = len(results_from_neo4j[0].get(column)) if any(length == 0 for length in columns_with_lengths.values()): if continue_if_no_results: log.warning(f"No paths were found in {kp} satisfying this query graph") else: log.error(f"No paths were found in {kp} satisfying this query graph", error_code="NoResults") return results_from_neo4j
def sort_results_by_confidence(self, message, response=None): # #### Set up the response object if one is not already available if response is None: if self.response is None: response = Response() else: response = self.response else: self.response = response self.message = message response.info("Re-sorting results by overal confidence metrics") #### Dead-simple sort, probably not very robust message.results.sort(key=lambda result: result.confidence, reverse=True)
def create_tabular_results(self, message, response=None): # #### Set up the response object if one is not already available if response is None: if self.response is None: response = Response() else: response = self.response else: self.response = response self.message = message response.info(f"Add simple tabular results to the Message") # #### Loop through the results[] adding row_data for that result for result in message.results: # #### For now, just the confidence, essence, and essence_type result.row_data = [ result.confidence, result.essence, result.essence_type ] #### Add table columns name message.table_column_names = ['confidence', 'essence', 'essence_type']
def apply(self, input_message, input_parameters, response=None): if response is None: response = Response() self.response = response self.message = input_message # Basic checks on arguments if not isinstance(input_parameters, dict): response.error("Provided parameters is not a dict", error_code="ParametersNotDict") return response # Define a complete set of allowed parameters and their defaults parameters = self.parameters parameters['kp'] = "ARAX/KG1" parameters['enforce_directionality'] = False parameters['use_synonyms'] = True parameters['synonym_handling'] = 'map_back' parameters['continue_if_no_results'] = False for key, value in input_parameters.items(): if key and key not in parameters: response.error(f"Supplied parameter {key} is not permitted", error_code="UnknownParameter") else: if type(value) is str and value.lower() == "true": value = True elif type(value) is str and value.lower() == "false": value = False parameters[key] = value # Default to expanding the entire query graph if the user didn't specify what to expand if not parameters['edge_id'] and not parameters['node_id']: parameters['edge_id'] = [ edge.id for edge in self.message.query_graph.edges ] parameters['node_id'] = self._get_orphan_query_node_ids( self.message.query_graph) if response.status != 'OK': return response response.data['parameters'] = parameters self.parameters = parameters # Do the actual expansion response.debug( f"Applying Expand to Message with parameters {parameters}") input_edge_ids = eu.convert_string_or_list_to_list( parameters['edge_id']) input_node_ids = eu.convert_string_or_list_to_list( parameters['node_id']) kp_to_use = self.parameters['kp'] continue_if_no_results = self.parameters['continue_if_no_results'] # Convert message knowledge graph to dictionary format, for faster processing dict_kg = eu.convert_standard_kg_to_dict_kg( self.message.knowledge_graph) # Expand any specified edges if input_edge_ids: query_sub_graph = self._extract_query_subgraph( input_edge_ids, self.message.query_graph) if response.status != 'OK': return response self.response.debug( f"Query graph for this Expand() call is: {query_sub_graph.to_dict()}" ) # Expand the query graph edge by edge (much faster for neo4j queries, and allows easy integration with BTE) ordered_qedges_to_expand = self._get_order_to_expand_edges_in( query_sub_graph) node_usages_by_edges_map = dict() for qedge in ordered_qedges_to_expand: answer_kg, edge_node_usage_map = self._expand_edge( qedge, kp_to_use, dict_kg, continue_if_no_results, self.message.query_graph) if response.status != 'OK': return response node_usages_by_edges_map[qedge.id] = edge_node_usage_map self._process_and_merge_answer(answer_kg, dict_kg) if response.status != 'OK': return response self._prune_dead_end_paths(dict_kg, query_sub_graph, node_usages_by_edges_map) if response.status != 'OK': return response # Expand any specified nodes if input_node_ids: for qnode_id in input_node_ids: answer_kg = self._expand_node(qnode_id, kp_to_use, continue_if_no_results, self.message.query_graph) if response.status != 'OK': return response self._process_and_merge_answer(answer_kg, dict_kg) if response.status != 'OK': return response # Convert message knowledge graph back to API standard format self.message.knowledge_graph = eu.convert_dict_kg_to_standard_kg( dict_kg) # Return the response and done kg = self.message.knowledge_graph response.info( f"After Expand, Message.KnowledgeGraph has {len(kg.nodes)} nodes and {len(kg.edges)} edges" ) return response
def _expand_edge( self, qedge: QEdge, kp_to_use: str, dict_kg: DictKnowledgeGraph, continue_if_no_results: bool, query_graph: QueryGraph, use_synonyms: bool, synonym_handling: str, log: Response ) -> Tuple[DictKnowledgeGraph, Dict[str, Dict[str, str]]]: # This function answers a single-edge (one-hop) query using the specified knowledge provider log.info(f"Expanding edge {qedge.id} using {kp_to_use}") answer_kg = DictKnowledgeGraph() edge_to_nodes_map = dict() # Create a query graph for this edge (that uses synonyms as well as curies found in prior steps) edge_query_graph = self._get_query_graph_for_edge( qedge, query_graph, dict_kg, use_synonyms, kp_to_use, log) if log.status != 'OK': return answer_kg, edge_to_nodes_map if not any(qnode for qnode in edge_query_graph.nodes if qnode.curie): log.error( f"Cannot expand an edge for which neither end has any curies. (Could not find curies to use from " f"a prior expand step, and neither qnode has a curie specified.)", error_code="InvalidQuery") return answer_kg, edge_to_nodes_map valid_kps = ["ARAX/KG1", "ARAX/KG2", "BTE", "COHD", "NGD"] if kp_to_use not in valid_kps: log.error( f"Invalid knowledge provider: {kp_to_use}. Valid options are {', '.join(valid_kps)}", error_code="InvalidKP") return answer_kg, edge_to_nodes_map else: if kp_to_use == 'BTE': from Expand.bte_querier import BTEQuerier kp_querier = BTEQuerier(log) elif kp_to_use == 'COHD': from Expand.COHD_querier import COHDQuerier kp_querier = COHDQuerier(log) elif kp_to_use == 'NGD': from Expand.ngd_querier import NGDQuerier kp_querier = NGDQuerier(log) else: from Expand.kg_querier import KGQuerier kp_querier = KGQuerier(log, kp_to_use) answer_kg, edge_to_nodes_map = kp_querier.answer_one_hop_query( edge_query_graph) if log.status != 'OK': return answer_kg, edge_to_nodes_map log.debug( f"Query for edge {qedge.id} returned results ({eu.get_printable_counts_by_qg_id(answer_kg)})" ) # Do some post-processing (deduplicate nodes, remove self-edges..) if synonym_handling != 'add_all': answer_kg, edge_to_nodes_map = self._deduplicate_nodes( answer_kg, edge_to_nodes_map, log) if eu.qg_is_fulfilled(edge_query_graph, answer_kg): answer_kg = self._remove_self_edges(answer_kg, edge_to_nodes_map, qedge.id, edge_query_graph.nodes, log) # Make sure our query has been fulfilled (unless we're continuing if no results) if not eu.qg_is_fulfilled(edge_query_graph, answer_kg): if continue_if_no_results: log.warning( f"No paths were found in {kp_to_use} satisfying this query graph" ) else: log.error( f"No paths were found in {kp_to_use} satisfying this query graph", error_code="NoResults") return answer_kg, edge_to_nodes_map
def parse(self, input_actions): #### Define a default response response = Response() response.info(f"Parsing input actions list") #### Basic error checking of the input_actions if not isinstance(input_actions, list): response.error("Provided input actions is not a list", error_code="ActionsNotList") return response if len(input_actions) == 0: response.error("Provided input actions is an empty list", error_code="ActionsListEmpty") return response #### Iterate through the list, checking the items actions = [] n_lines = 1 for action in input_actions: response.debug(f"Parsing action: {action}") # If this line is empty, then skip match = re.match(r"\s*$", action) if match: continue # If this line begins with a #, it is a comment, then skip match = re.match(r"#", action) if match: continue #### First look for a naked command without parentheses match = re.match(r"\s*([A-Za-z_]+)\s*$", action) if match is not None: action = { "line": n_lines, "command": match.group(1), "parameters": None } actions.append(action) #### Then look for and parse a command with parentheses and a comma-separated parameter list if match is None: match = re.match(r"\s*([A-Za-z_]+)\((.*)\)\s*$", action) if match is not None: command = match.group(1) param_string = match.group(2) #### Split the parameters on comma and process those param_string_list = re.split(",", param_string) parameters = {} #### If a value is of the form key=[value1,value2] special code is needed to recompose that mode = 'normal' list_buffer = [] key = '' for param_item in param_string_list: param_item = param_item.strip() if mode == 'normal': #### Split on the first = only (might be = in the value) values = re.split("=", param_item, 1) key = values[0] #### If there isn't a value after an =, then just set to string true value = 'true' if len(values) > 1: value = values[1] key = key.strip() value = value.strip() #### If the value begins with a "[", then this is a list match = re.match(r"\[(.+)$", value) if match: #### If it also ends with a "]", then this is a list of one element match2 = re.match(r"\[(.*)\]$", value) if match2: if match2.group(1) == '': parameters[key] = [] else: parameters[key] = [match2.group(1)] else: mode = 'in_list' list_buffer = [match.group(1)] else: parameters[key] = value #### Special processing if we're in the middle of a list elif mode == 'in_list': match = re.match(r"(.*)\]$", param_item) if match: mode = 'normal' list_buffer.append(match.group(1)) parameters[key] = list_buffer else: list_buffer.append(param_item) else: eprint("Inconceivable!") if mode == 'in_list': parameters[key] = list_buffer #### Store the parsed result in a dict and add to the list action = { "line": n_lines, "command": command, "parameters": parameters } actions.append(action) else: response.error(f"Unable to parse action {action}", error_code="ActionsListEmpty") n_lines += 1 #### Put the actions in the response data envelope and return response.data["actions"] = actions return response
def reassign_curies(self, message, input_parameters, describe=False): """ Reassigns CURIEs to the target Knowledge Provider :param message: Translator standard Message object :type message: Message :param input_parameters: Dict of input parameters to control the method :type input_parameters: Message :return: Response object with execution information :rtype: Response """ # #### Internal documentation setup allowable_parameters = { 'knowledge_provider': { 'Name of the Knowledge Provider CURIE space to map to. Default=KG1. Also currently supported KG2' }, 'mismap_result': { 'Desired action when mapping fails: ERROR or WARNING. Default is ERROR' }, } if describe: allowable_parameters[ 'dsl_command'] = '`reassign_curies()`' # can't get this name at run-time, need to manually put it in per https://www.python.org/dev/peps/pep-3130/ allowable_parameters[ 'brief_description'] = """The `reassign_curies` method reassigns all the CURIEs in the Message QueryGraph to the specified knowledge provider. Allowed values are KG1 or KG2. Default is KG1 if not specified.""" return allowable_parameters #### Define a default response response = Response() self.response = response self.message = message #### Basic checks on arguments if not isinstance(input_parameters, dict): response.error("Provided parameters is not a dict", error_code="ParametersNotDict") return response #### Define a complete set of allowed parameters and their defaults parameters = { 'knowledge_provider': 'KG1', 'mismap_result': 'ERROR', } #### Loop through the input_parameters and override the defaults and make sure they are allowed for key, value in input_parameters.items(): if key not in parameters: response.error(f"Supplied parameter {key} is not permitted", error_code="UnknownParameter") else: parameters[key] = value #### Return if any of the parameters generated an error (showing not just the first one) if response.status != 'OK': return response #### Store these final parameters for convenience response.data['parameters'] = parameters self.parameters = parameters # Check that the knowledge_provider is valid: if parameters['knowledge_provider'] != 'KG1' and parameters[ 'knowledge_provider'] != 'KG2': response.error( f"Specified knowledge provider must be 'KG1' or 'KG2', not '{parameters['knowledge_provider']}'", error_code="UnknownKP") return response #### Now try to assign the CURIEs response.info( f"Reassigning the CURIEs in QueryGraph to {parameters['knowledge_provider']} space" ) #### Make sure there's a query_graph already here if message.query_graph is None: message.query_graph = QueryGraph() message.query_graph.nodes = [] message.query_graph.edges = [] if message.query_graph.nodes is None: message.query_graph.nodes = [] #### Set up the KGNodeIndex kgNodeIndex = KGNodeIndex() # Loops through the QueryGraph nodes and adjust them for qnode in message.query_graph.nodes: # If the CURIE is None, then there's nothing to do curie = qnode.curie if curie is None: continue # Map the CURIE to the desired Knowledge Provider if parameters['knowledge_provider'] == 'KG1': if kgNodeIndex.is_curie_present(curie) is True: mapped_curies = [curie] else: mapped_curies = kgNodeIndex.get_KG1_curies(curie) elif parameters['knowledge_provider'] == 'KG2': if kgNodeIndex.is_curie_present(curie, kg_name='KG2'): mapped_curies = [curie] else: mapped_curies = kgNodeIndex.get_curies_and_types( curie, kg_name='KG2') else: response.error( f"Specified knowledge provider must be 'KG1' or 'KG2', not '{parameters['knowledge_provider']}'", error_code="UnknownKP") return response # Try to find a new CURIE new_curie = None if len(mapped_curies) == 0: if parameters['mismap_result'] == 'WARNING': response.warning( f"Did not find a mapping for {curie} to KP '{parameters['knowledge_provider']}'. Leaving as is" ) else: response.error( f"Did not find a mapping for {curie} to KP '{parameters['knowledge_provider']}'. This is an error" ) elif len(mapped_curies) == 1: new_curie = mapped_curies[0] else: original_curie_is_fine = False for potential_curie in mapped_curies: if potential_curie == curie: original_curie_is_fine = True if original_curie_is_fine: new_curie = curie else: new_curie = mapped_curies[0] response.warning( f"There are multiple possible CURIEs in KP '{parameters['knowledge_provider']}'. Selecting the first one {new_curie}" ) # If there's no CURIE, then nothing to do if new_curie is None: pass # If it's the same elif new_curie == curie: response.debug( f"CURIE {curie} is fine for KP '{parameters['knowledge_provider']}'" ) else: response.info( f"Remapping CURIE {curie} to {new_curie} for KP '{parameters['knowledge_provider']}'" ) #### Return the response return response
def add_qedge(self, message, input_parameters, describe=False): """ Adds a new QEdge object to the QueryGraph inside the Message object :return: Response object with execution information :rtype: Response """ # #### Internal documentation setup allowable_parameters = { 'id': { 'Any string that is unique among all QEdge id fields, with recommended format e00, e01, e02, etc.' }, 'source_id': { 'id of the source QNode already present in the QueryGraph (e.g. n01, n02)' }, 'target_id': { 'id of the target QNode already present in the QueryGraph (e.g. n01, n02)' }, 'type': { 'Any valid Translator/BioLink relationship type (e.g. physically_interacts_with, participates_in)' }, } if describe: #allowable_parameters['action'] = { 'None' } #allowable_parameters = dict() allowable_parameters[ 'dsl_command'] = '`add_qedge()`' # can't get this name at run-time, need to manually put it in per https://www.python.org/dev/peps/pep-3130/ allowable_parameters[ 'brief_description'] = """The `add_qedge` method adds an additional QEdge to the QueryGraph in the Message object. Currently source_id and target_id QNodes must already be present in the QueryGraph. The specified type is not currently checked that it is a valid Translator/BioLink relationship type, but it should be.""" return allowable_parameters #### Define a default response response = Response() self.response = response self.message = message #### Basic checks on arguments if not isinstance(input_parameters, dict): response.error("Provided parameters is not a dict", error_code="ParametersNotDict") return response #### Define a complete set of allowed parameters and their defaults parameters = { 'id': None, 'source_id': None, 'target_id': None, 'type': None, } #### Loop through the input_parameters and override the defaults and make sure they are allowed for key, value in input_parameters.items(): if key not in parameters: response.error(f"Supplied parameter {key} is not permitted", error_code="UnknownParameter") else: parameters[key] = value #### Return if any of the parameters generated an error (showing not just the first one) if response.status != 'OK': return response #### Store these final parameters for convenience response.data['parameters'] = parameters self.parameters = parameters #### Now apply the filters. Order of operations is probably quite important #### Scalar value filters probably come first like minimum_confidence, then complex logic filters #### based on edge or node properties, and then finally maximum_results response.info( f"Adding a QueryEdge to Message with parameters {parameters}") #### Make sure there's a query_graph already here if message.query_graph is None: message.query_graph = QueryGraph() message.query_graph.nodes = [] message.query_graph.edges = [] if message.query_graph.edges is None: message.query_graph.edges = [] #### Create a QEdge qedge = QEdge() if parameters['id'] is not None: id = parameters['id'] else: id = self.__get_next_free_edge_id() qedge.id = id #### Get the list of available node_ids qnodes = message.query_graph.nodes ids = {} for qnode in qnodes: id = qnode.id ids[id] = 1 #### Add the source_id if parameters['source_id'] is not None: if parameters['source_id'] not in ids: response.error( f"While trying to add QEdge, there is no QNode with id {parameters['source_id']}", error_code="UnknownSourceId") return response qedge.source_id = parameters['source_id'] else: response.error( f"While trying to add QEdge, source_id is a required parameter", error_code="MissingSourceId") return response #### Add the target_id if parameters['target_id'] is not None: if parameters['target_id'] not in ids: response.error( f"While trying to add QEdge, there is no QNode with id {parameters['target_id']}", error_code="UnknownTargetId") return response qedge.target_id = parameters['target_id'] else: response.error( f"While trying to add QEdge, target_id is a required parameter", error_code="MissingTargetId") return response #### Add the type if any. Need to verify it's an allowed type. FIXME if parameters['type'] is not None: qedge.type = parameters['type'] #### Add it to the query_graph edge list message.query_graph.edges.append(qedge) #### Return the response return response
def add_qnode(self, message, input_parameters, describe=False): """ Adds a new QNode object to the QueryGraph inside the Message object :return: Response object with execution information :rtype: Response """ # #### Internal documentation setup allowable_parameters = { 'id': { 'Any string that is unique among all QNode id fields, with recommended format n00, n01, n02, etc.' }, 'curie': { 'Any compact URI (CURIE) (e.g. DOID:9281) (May also be a list like [UniProtKB:P12345,UniProtKB:Q54321])' }, 'name': { 'Any name of a bioentity that will be resolved into a CURIE if possible or result in an error if not (e.g. hypertension, insulin)' }, 'type': { 'Any valid Translator bioentity type (e.g. protein, chemical_substance, disease)' }, 'is_set': { 'If set to true, this QNode represents a set of nodes that are all in common between the two other linked QNodes' }, } if describe: allowable_parameters[ 'dsl_command'] = '`add_qnode()`' # can't get this name at run-time, need to manually put it in per https://www.python.org/dev/peps/pep-3130/ allowable_parameters[ 'brief_description'] = """The `add_qnode` method adds an additional QNode to the QueryGraph in the Message object. Currently when a curie or name is specified, this method will only return success if a matching node is found in the KG1/KG2 KGNodeIndex.""" return allowable_parameters #### Define a default response response = Response() self.response = response self.message = message #### Basic checks on arguments if not isinstance(input_parameters, dict): response.error("Provided parameters is not a dict", error_code="ParametersNotDict") return response #### Define a complete set of allowed parameters and their defaults parameters = { 'id': None, 'curie': None, 'name': None, 'type': None, 'is_set': None, } #### Loop through the input_parameters and override the defaults and make sure they are allowed for key, value in input_parameters.items(): if key not in parameters: response.error(f"Supplied parameter {key} is not permitted", error_code="UnknownParameter") else: parameters[key] = value #### Return if any of the parameters generated an error (showing not just the first one) if response.status != 'OK': return response #### Store these final parameters for convenience response.data['parameters'] = parameters self.parameters = parameters #### Now apply the filters. Order of operations is probably quite important #### Scalar value filters probably come first like minimum_confidence, then complex logic filters #### based on edge or node properties, and then finally maximum_results response.info( f"Adding a QueryNode to Message with parameters {parameters}") #### Make sure there's a query_graph already here if message.query_graph is None: message.query_graph = QueryGraph() message.query_graph.nodes = [] message.query_graph.edges = [] if message.query_graph.nodes is None: message.query_graph.nodes = [] #### Set up the KGNodeIndex kgNodeIndex = KGNodeIndex() # Create the QNode and set the id qnode = QNode() if parameters['id'] is not None: id = parameters['id'] else: id = self.__get_next_free_node_id() qnode.id = id # Set the is_set parameter to what the user selected if parameters['is_set'] is not None: qnode.is_set = (parameters['is_set'].lower() == 'true') #### If the CURIE is specified, try to find that if parameters['curie'] is not None: # If the curie is a scalar then treat it here as a list of one if isinstance(parameters['curie'], str): curie_list = [parameters['curie']] is_curie_a_list = False if parameters['is_set'] is not None and qnode.is_set is True: response.error( f"Specified CURIE '{parameters['curie']}' is a scalar, but is_set=true, which doesn't make sense", error_code="CurieScalarButIsSetTrue") return response # Or else set it up as a list elif isinstance(parameters['curie'], list): curie_list = parameters['curie'] is_curie_a_list = True qnode.curie = [] if parameters['is_set'] is None: response.warning( f"Specified CURIE '{parameters['curie']}' is a list, but is_set was not set to true. It must be true in this context, so automatically setting to true. Avoid this warning by explictly setting to true." ) qnode.is_set = True else: if qnode.is_set == False: response.warning( f"Specified CURIE '{parameters['curie']}' is a list, but is_set=false, which doesn't make sense, so automatically setting to true. Avoid this warning by explictly setting to true." ) qnode.is_set = True # Or if it's neither a list or a string, then error out. This cannot be handled at present else: response.error( f"Specified CURIE '{parameters['curie']}' is neither a string nor a list. This cannot to handled", error_code="CurieNotListOrScalar") return response # Loop over the available curies and create the list for curie in curie_list: response.debug(f"Looking up CURIE {curie} in KgNodeIndex") nodes = kgNodeIndex.get_curies_and_types(curie, kg_name='KG2') # If nothing was found, we won't bail out, but rather just issue a warning if len(nodes) == 0: response.warning( f"A node with CURIE {curie} is not in our knowledge graph KG2, but will continue" ) if is_curie_a_list: qnode.curie.append(curie) else: qnode.curie = curie else: # FIXME. This is just always taking the first result. This could cause problems for CURIEs with multiple types. Is that possible? # In issue #623 on 2020-06-15 we concluded that we should not specify the type here #qnode.type = nodes[0]['type'] # Either append or set the found curie if is_curie_a_list: qnode.curie.append(nodes[0]['curie']) else: qnode.curie = nodes[0]['curie'] if 'type' in parameters and parameters['type'] is not None: if isinstance(parameters['type'], str): qnode.type = parameters['type'] else: qnode.type = parameters['type'][0] message.query_graph.nodes.append(qnode) return response #### If the name is specified, try to find that if parameters['name'] is not None: response.debug( f"Looking up CURIE {parameters['name']} in KgNodeIndex") nodes = kgNodeIndex.get_curies_and_types(parameters['name']) if len(nodes) == 0: nodes = kgNodeIndex.get_curies_and_types(parameters['name'], kg_name='KG2') if len(nodes) == 0: response.error( f"A node with name '{parameters['name']}'' is not in our knowledge graph", error_code="UnknownCURIE") return response qnode.curie = nodes[0]['curie'] qnode.type = nodes[0]['type'] message.query_graph.nodes.append(qnode) return response #### If the type is specified, just add that type. There should be checking that it is legal. FIXME if parameters['type'] is not None: qnode.type = parameters['type'] if parameters['is_set'] is not None: qnode.is_set = (parameters['is_set'].lower() == 'true') message.query_graph.nodes.append(qnode) return response #### If we get here, it means that all three main parameters are null. Just a generic node with no type or anything. This is okay. message.query_graph.nodes.append(qnode) return response
def aggregate_scores(self, message, response=None): # #### Set up the response object if one is not already available if response is None: if self.response is None: response = Response() else: response = self.response else: self.response = response self.message = message # #### Compute some basic information about the query_graph query_graph_info = QueryGraphInfo() result = query_graph_info.assess(message) #response.merge(result) #if result.status != 'OK': # print(response.show(level=Response.DEBUG)) # return response # DMK FIXME: This need to be refactored so that: # 1. The attribute names are dynamically mapped to functions that handle their weightings (for ease of renaming attribute names) # 2. Weighting of individual attributes (eg. "probability" should be trusted MUCH less than "probability_treats") # 3. Auto-handling of normalizing scores to be in [0,1] (eg. observed_expected ration \in (-inf, inf) while probability \in (0,1) # 4. Auto-thresholding of values (eg. if chi_square <0.05, penalize the most, if probability_treats < 0.8, penalize the most, etc.) # 5. Allow for ranked answers (eg. observed_expected can have a single, huge value, skewing the rest of them # #### Iterate through all the edges in the knowledge graph to: # #### 1) Create a dict of all edges by id # #### 2) Collect some min,max stats for edge_attributes that we may need later kg_edges = {} score_stats = {} for edge in message.knowledge_graph.edges: kg_edges[edge.id] = edge if edge.edge_attributes is not None: for edge_attribute in edge.edge_attributes: # FIXME: DMK: We should probably have some some way to dynamically get the attribute names since they appear to be constantly changing # DMK: Crazy idea: have the individual ARAXi commands pass along their attribute names along with what they think of is a good way to handle them # DMK: eg. "higher is better" or "my range of [0, inf]" or "my value is a probability", etc. for attribute_name in [ 'probability', 'normalized_google_distance', 'jaccard_index', 'probability_treats', 'paired_concept_frequency', 'observed_expected_ratio', 'chi_square' ]: if edge_attribute.name == attribute_name: if attribute_name not in score_stats: score_stats[attribute_name] = { 'minimum': None, 'maximum': None } # FIXME: doesn't handle the case when all values are inf or NaN value = float(edge_attribute.value) # TODO: don't set to max here, since returning inf for some edge attributes means "I have no data" #if np.isinf(value): # value = 9999 # initialize if not None already if not np.isinf(value) and not np.isinf( -value) and not np.isnan( value): # Ignore inf, -inf, and nan if not score_stats[attribute_name]['minimum']: score_stats[attribute_name][ 'minimum'] = value if not score_stats[attribute_name]['maximum']: score_stats[attribute_name][ 'maximum'] = value if value > score_stats[attribute_name][ 'maximum']: # DMK FIXME: expected type 'float', got 'None' instead score_stats[attribute_name][ 'maximum'] = value if value < score_stats[attribute_name][ 'minimum']: # DMK FIXME: expected type 'float', got 'None' instead score_stats[attribute_name][ 'minimum'] = value response.info(f"Summary of available edge metrics: {score_stats}") # #### Loop through the results[] in order to compute aggregated scores i_result = 0 for result in message.results: #response.debug(f"Metrics for result {i_result} {result.essence}: ") # #### Begin with a default score of 1.0 for everything score = 1.0 # #### There are often many edges associated with a result[]. Some are great, some are terrible. # #### For now, the score will be based on the best one. Maybe combining probabilities in quadrature would be better best_probability = 0.0 # TODO: What's this? the best probability of what? eps = np.finfo(np.float).eps # epsilon to avoid division by 0 penalize_factor = 0.7 # multiplicative factor to penalize by if the KS/KP return NaN or Inf indicating they haven't seen it before # #### Loop through each edge in the result for edge in result.edge_bindings: kg_edge_id = edge.kg_id # #### Set up a string buffer to keep some debugging information that could be printed buf = '' # #### If the edge has a confidence value, then multiply that into the final score if kg_edges[kg_edge_id].confidence is not None: buf += f" confidence={kg_edges[kg_edge_id].confidence}" score *= float(kg_edges[kg_edge_id].confidence) # #### If the edge has attributes, loop through those looking for scores that we know how to handle if kg_edges[kg_edge_id].edge_attributes is not None: for edge_attribute in kg_edges[kg_edge_id].edge_attributes: # FIXME: These are chemical_substance->protein binding probabilities, may not want be treating them like this.... #### EWD: Vlado has suggested that any of these links with chemical_substance->protein binding probabilities are #### EWD: mostly junk. very low probablility of being correct. His opinion seemed to be that they shouldn't be in the KG #### EWD: If we keep them, maybe their probabilities should be knocked down even further, in half, in quarter.. # DMK: I agree: hence why I said we should probably not be treating them like this (and not trusting them a lot) # #### If the edge_attribute is named 'probability', then for now use it to record the best probability only if edge_attribute.name == 'probability': value = float(edge_attribute.value) buf += f" probability={edge_attribute.value}" if value > best_probability: best_probability = value # #### If the edge_attribute is named 'probability_drug_treats', then for now we won't do anything # #### because this value also seems to be copied into the edge confidence field, so is already # #### taken into account #if edge_attribute.name == 'probability_drug_treats': # this is already put in confidence # buf += f" probability_drug_treats={edge_attribute.value}" # score *= value # DMK FIXME: Do we actually have 'probability_drug_treats' attributes?, the probability_drug_treats is *not* put in the confidence see: confidence = None in `predict_drug_treats_disease.py` # DMK: also note the edge type is: edge_type = "probably_treats" # If the edge_attribute is named 'probability_treats', use the value more or less as a probability #### EWD says: but note that when I last worked on this, the probability_treats was repeated in an edge attribute #### EWD says: as well as in the edge confidence score, so I commented out this section (see immediately above) DMK (same re: comment above :) ) #### EWD says: so that it wouldn't be counted twice. But that may have changed in the mean time. if edge_attribute.name == "probability_treats": prob_treats = float(edge_attribute.value) # Don't treat as a good prediction if the ML model returns a low value if prob_treats < penalize_factor: factor = penalize_factor else: factor = prob_treats score *= factor # already a number between 0 and 1, so just multiply # #### If the edge_attribute is named 'ngd', then use some hocus pocus to convert to a confidence if edge_attribute.name == 'normalized_google_distance': ngd = float(edge_attribute.value) # If the distance is infinite, then set it to 10, a very large number in this context if np.isinf(ngd): ngd = 10.0 buf += f" ngd={ngd}" # #### Apply a somewhat arbitrary transformation such that: # #### NGD = 0.3 leads to a factor of 1.0. That's *really* close # #### NGD = 0.5 leads to a factor of 0.88. That still a close NGD # #### NGD = 0.7 leads to a factor of 0.76. Same ballpark # #### NGD = 0.9 this is pretty far away. Still the factor is 0.64. Distantly related # #### NGD = 1.0 is very far. Still, factor is 0.58. Grade inflation is rampant. factor = 1 - (ngd - 0.3) * 0.6 # Apply limits of 1.0 and 0.01 to the linear fudge if factor < 0.01: factor = 0.01 if factor > 1: factor = 1.0 buf += f" ngd_factor={factor}" score *= factor # #### If the edge_attribute is named 'jaccard_index', then use some hocus pocus to convert to a confidence if edge_attribute.name == 'jaccard_index': jaccard = float(edge_attribute.value) # If the jaccard index is infinite, set to some arbitrarily bad score if np.isinf(jaccard): jaccard = 0.01 # #### Set the confidence factor so that the best value of all results here becomes 0.95 # #### Why not 1.0? Seems like in scenarios where we're computing a Jaccard index, nothing is really certain factor = jaccard / score_stats['jaccard_index'][ 'maximum'] * 0.95 buf += f" jaccard={jaccard}, factor={factor}" score *= factor # If the edge_attribute is named 'paired_concept_frequency', then ... if edge_attribute.name == "paired_concept_frequency": paired_concept_freq = float(edge_attribute.value) if np.isinf(paired_concept_freq) or np.isnan( paired_concept_freq): factor = penalize_factor else: try: factor = paired_concept_freq / score_stats[ 'paired_concept_frequency']['maximum'] except: factor = paired_concept_freq / ( score_stats['paired_concept_frequency'] ['maximum'] + eps) score *= factor buf += f" paired_concept_frequency={paired_concept_freq}, factor={factor}" # If the edge_attribute is named 'observed_expected_ratio', then ... if edge_attribute.name == 'observed_expected_ratio': obs_exp_ratio = float(edge_attribute.value) if np.isinf(obs_exp_ratio) or np.isnan( obs_exp_ratio): factor = penalize_factor # Penalize for missing info # Would love to throw this into a sigmoid like function customized by the max value observed # for now, just throw into a sigmoid and see what happens factor = 1 / float(1 + np.exp(-4 * obs_exp_ratio)) score *= factor buf += f" observed_expected_ratio={obs_exp_ratio}, factor={factor}" # If the edge_attribute is named 'chi_square', then compute a factor based on the chisq and the max chisq if edge_attribute.name == 'chi_square': chi_square = float(edge_attribute.value) if np.isinf(chi_square) or np.isnan(chi_square): factor = penalize_factor else: try: factor = 1 - ( chi_square / score_stats['chi_square']['maximum'] ) # lower is better except: factor = 1 - ( chi_square / (score_stats['chi_square']['maximum'] + eps)) # lower is better score *= factor buf += f" chi_square={chi_square}, factor={factor}" # #### When debugging, log the edge_id and the accumulated information in the buffer #response.debug(f" - {kg_edge_id} {buf}") # #### If there was a best_probability recorded, then multiply into the running score #### EWD: This was commented out by DMK? I don't know why. I think it should be here FIXME #if best_probability > 0.0: # score *= best_probability # DMK: for some reason, this was causing my scores to be ridiculously low, so I commented it out and confidences went up "quite a bit" # #### Make all scores at least 0.01. This is all way low anyway, but let's not have anything that rounds to zero # #### This is a little bad in that 0.005 becomes better than 0.011, but this is all way low, so who cares if score < 0.01: score += 0.01 #### Round to reasonable precision. Keep only 3 digits after the decimal score = int(score * 1000 + 0.5) / 1000.0 #response.debug(f" ---> final score={score}") result.confidence = score result.row_data = [score, result.essence, result.essence_type] i_result += 1 #### Add table columns name message.table_column_names = ['confidence', 'essence', 'essence_type'] #### Re-sort the final results message.results.sort(key=lambda result: result.confidence, reverse=True)
def check_for_query_graph_tags(self, message, query_graph_info): #### Define a default response response = Response() self.response = response self.message = message response.debug(f"Checking KnowledgeGraph for QueryGraph tags") #### Get shorter handles knowedge_graph = message.knowledge_graph nodes = knowedge_graph.nodes edges = knowedge_graph.edges #### Store number of nodes and edges self.n_nodes = len(nodes) self.n_edges = len(edges) response.debug(f"Found {self.n_nodes} nodes and {self.n_edges} edges") #### Clear the maps self.node_map = {'by_qnode_id': {}} self.edge_map = {'by_qedge_id': {}} #### Loop through nodes computing some stats n_nodes_with_query_graph_ids = 0 for node in nodes: id = node.id if node.qnode_id is None: continue n_nodes_with_query_graph_ids += 1 #### Place an entry in the node_map if node.qnode_id not in self.node_map['by_qnode_id']: self.node_map['by_qnode_id'][node.qnode_id] = {} self.node_map['by_qnode_id'][node.qnode_id][id] = 1 #### Tally the stats if n_nodes_with_query_graph_ids == self.n_nodes: self.query_graph_id_node_status = 'all nodes have query_graph_ids' elif n_nodes_with_query_graph_ids == 0: self.query_graph_id_node_status = 'no nodes have query_graph_ids' else: self.query_graph_id_node_status = 'only some nodes have query_graph_ids' response.info( f"In the KnowledgeGraph, {self.query_graph_id_node_status}") #### Loop through edges computing some stats n_edges_with_query_graph_ids = 0 for edge in edges: id = edge.id if edge.qedge_id is None: continue n_edges_with_query_graph_ids += 1 #### Place an entry in the edge_map if edge.qedge_id not in self.edge_map['by_qedge_id']: self.edge_map['by_qedge_id'][edge.qedge_id] = {} self.edge_map['by_qedge_id'][edge.qedge_id][id] = 1 if n_edges_with_query_graph_ids == self.n_edges: self.query_graph_id_edge_status = 'all edges have query_graph_ids' elif n_edges_with_query_graph_ids == 0: self.query_graph_id_edge_status = 'no edges have query_graph_ids' else: self.query_graph_id_edge_status = 'only some edges have query_graph_ids' response.info( f"In the KnowledgeGraph, {self.query_graph_id_edge_status}") #### Return the response return response
def assess(self, message): #### Define a default response response = Response() self.response = response self.message = message response.debug(f"Assessing the QueryGraph for basic information") #### Get shorter handles query_graph = message.query_graph nodes = query_graph.nodes edges = query_graph.edges #### Store number of nodes and edges self.n_nodes = len(nodes) self.n_edges = len(edges) response.debug(f"Found {self.n_nodes} nodes and {self.n_edges} edges") #### Handle impossible cases if self.n_nodes == 0: response.error( "QueryGraph has 0 nodes. At least 1 node is required", error_code="QueryGraphZeroNodes") return response if self.n_nodes == 1 and self.n_edges > 0: response.error( "QueryGraph may not have edges if there is only one node", error_code="QueryGraphTooManyEdges") return response #if self.n_nodes == 2 and self.n_edges > 1: # response.error("QueryGraph may not have more than 1 edge if there are only 2 nodes", error_code="QueryGraphTooManyEdges") # return response #### Loop through nodes computing some stats node_info = {} self.node_type_map = {} for qnode in nodes: id = qnode.id node_info[id] = { 'id': id, 'node_object': qnode, 'has_curie': False, 'type': qnode.type, 'has_type': False, 'is_set': False, 'n_edges': 0, 'n_links': 0, 'is_connected': False, 'edges': [], 'edge_dict': {} } if qnode.curie is not None: node_info[id]['has_curie'] = True if qnode.type is not None: node_info[id]['has_type'] = True #if qnode.is_set is not None: node_info[id]['is_set'] = True if qnode.id is None: response.error( "QueryGraph has a node with no id. This is not permitted", error_code="QueryGraphNodeWithNoId") return response #### Store lookup of types warning_counter = 0 if qnode.type is None: if warning_counter == 0: response.debug( "QueryGraph has nodes with no type. This may cause problems with results inference later" ) warning_counter += 1 self.node_type_map['unknown'] = id else: self.node_type_map[qnode.type] = id #### Loop through edges computing some stats edge_info = {} self.edge_type_map = {} unique_links = {} for qedge in edges: #### Ignore special informationational edges for now. virtual_edge_types = { 'has_normalized_google_distance_with': 1, 'has_fisher_exact_test_p-value_with': 1, 'has_jaccard_index_with': 1, 'probably_treats': 1, 'has_paired_concept_frequency_with': 1, 'has_observed_expected_ratio_with': 1, 'has_chi_square_with': 1 } if qedge.type is not None and qedge.type in virtual_edge_types: continue id = qedge.id edge_info[id] = { 'id': id, 'has_type': False, 'source_id': qedge.source_id, 'target_id': qedge.target_id, 'type': None } #if qnode.type is not None: if qedge.type is not None: edge_info[id]['has_type'] = True edge_info[id]['type'] = qnode.type if qedge.id is None: response.error( "QueryGraph has a edge with no id. This is not permitted", error_code="QueryGraphEdgeWithNoId") return response #### Create a unique node link string link_string = ','.join(sorted([qedge.source_id, qedge.target_id])) if link_string not in unique_links: node_info[qedge.source_id]['n_links'] += 1 node_info[qedge.target_id]['n_links'] += 1 unique_links[link_string] = 1 #print(link_string) node_info[qedge.source_id]['n_edges'] += 1 node_info[qedge.target_id]['n_edges'] += 1 node_info[qedge.source_id]['is_connected'] = True node_info[qedge.target_id]['is_connected'] = True #node_info[qedge.source_id]['edges'].append(edge_info[id]) #node_info[qedge.target_id]['edges'].append(edge_info[id]) node_info[qedge.source_id]['edges'].append(edge_info[id]) node_info[qedge.target_id]['edges'].append(edge_info[id]) node_info[qedge.source_id]['edge_dict'][id] = edge_info[id] node_info[qedge.target_id]['edge_dict'][id] = edge_info[id] #### Store lookup of types warning_counter = 0 edge_type = 'any' if qedge.type is None: if warning_counter == 0: response.debug( "QueryGraph has edges with no type. This may cause problems with results inference later" ) warning_counter += 1 else: edge_type = qedge.type #### It's not clear yet whether we need to store the whole sentence or just the type #type_encoding = f"{node_info[qedge.source_id]['type']}---{edge_type}---{node_info[qedge.target_id]['type']}" type_encoding = edge_type self.edge_type_map[type_encoding] = id #### Loop through the nodes again, trying to identify the start_node and the end_node singletons = [] for node_id, node_data in node_info.items(): if node_data['n_links'] < 2: singletons.append(node_data) elif node_data['n_links'] > 2: self.is_bifurcated_graph = True response.warning( "QueryGraph appears to have a fork in it. This might cause trouble" ) #### Try to identify the start_node and the end_node start_node = singletons[0] if len(nodes) == 1: # Just a single node, fine pass elif len(singletons) < 2: response.warning( "QueryGraph appears to be circular or has a strange geometry. This might cause trouble" ) elif len(singletons) > 2: response.warning( "QueryGraph appears to have a fork in it. This might cause trouble" ) else: if singletons[0]['has_curie'] is True and singletons[1][ 'has_curie'] is False: start_node = singletons[0] elif singletons[0]['has_curie'] is False and singletons[1][ 'has_curie'] is True: start_node = singletons[1] else: start_node = singletons[0] #### Hmm, that's not very robust against odd graphs. This needs work. FIXME self.node_info = node_info self.edge_info = edge_info self.start_node = start_node current_node = start_node node_order = [start_node] edge_order = [] edges = current_node['edges'] while 1: #tmp = { 'astate': '1', 'current_node': current_node, 'node_order': node_order, 'edge_order': edge_order, 'edges': edges } #print(json.dumps(ast.literal_eval(repr(tmp)),sort_keys=True,indent=2)) #print('==================================================================================') #tmp = input() if len(edges) == 0: break if len(edges) > 1: response.error( "Help, two edges at A583. Don't know what to do", error_code="InteralErrorA583") return response edge_order.append(edges[0]) previous_node = current_node if edges[0]['source_id'] == current_node['id']: current_node = node_info[edges[0]['target_id']] elif edges[0]['target_id'] == current_node['id']: current_node = node_info[edges[0]['source_id']] else: response.error("Help, edge error A584. Don't know what to do", error_code="InteralErrorA584") return response node_order.append(current_node) #tmp = { 'astate': '2', 'current_node': current_node, 'node_order': node_order, 'edge_order': edge_order, 'edges': edges } #print(json.dumps(ast.literal_eval(repr(tmp)),sort_keys=True,indent=2)) #print('==================================================================================') #tmp = input() edges = current_node['edges'] new_edges = [] for edge in edges: if edge['id'] not in previous_node['edge_dict']: new_edges.append(edge) edges = new_edges if len(edges) == 0: break #tmp = { 'astate': '3', 'current_node': current_node, 'node_order': node_order, 'edge_order': edge_order, 'edges': edges } #print(json.dumps(ast.literal_eval(repr(tmp)),sort_keys=True,indent=2)) #print('==================================================================================') #tmp = input() self.node_order = node_order self.edge_order = edge_order # Create a text rendering of the QueryGraph geometry for matching against a template self.query_graph_templates = { 'simple': '', 'detailed': { 'n_nodes': len(node_order), 'components': [] } } node_index = 0 edge_index = 0 #print(json.dumps(ast.literal_eval(repr(node_order)),sort_keys=True,indent=2)) for node in node_order: component_id = f"n{node_index:02}" content = '' component = { 'component_type': 'node', 'component_id': component_id, 'has_curie': node['has_curie'], 'has_type': node['has_type'], 'type_value': None } self.query_graph_templates['detailed']['components'].append( component) if node['has_curie']: content = 'curie' if node['has_type'] and node['node_object'].type is not None: content = f"type={node['node_object'].type}" component['type_value'] = node['node_object'].type elif node['has_type']: content = 'type' template_part = f"{component_id}({content})" self.query_graph_templates['simple'] += template_part # Since queries with intermediate nodes that are not is_set=true tend to blow up, for now, make them is_set=true unless explicitly set to false if node_index > 0 and node_index < (self.n_nodes - 1): if 'is_set' not in node or node['is_set'] is None: node['node_object'].is_set = True response.warning( f"Setting unspecified is_set to true for {node['id']} because this will probably lead to a happier result" ) elif node['is_set'] is True: response.debug( f"Value for is_set is already true for {node['id']} so that's good" ) elif node['is_set'] is False: #response.info(f"Value for is_set is set to false for intermediate node {node['id']}. This could lead to weird results. Consider setting it to true") response.info( f"Value for is_set is false for intermediate node {node['id']}. Setting to true because this will probably lead to a happier result" ) node['node_object'].is_set = True #else: # response.error(f"Unrecognized value is_set='{node['is_set']}' for {node['id']}. This should be true or false") node_index += 1 if node_index < self.n_nodes: component_id = f"e{edge_index:02}" template_part = f"-{component_id}()-" self.query_graph_templates['simple'] += template_part component = { 'component_type': 'edge', 'component_id': component_id, 'has_curie': False, 'has_type': False } self.query_graph_templates['detailed']['components'].append( component) edge_index += 1 response.debug( f"The QueryGraph reference template is: {self.query_graph_templates['simple']}" ) #tmp = { 'node_info': node_info, 'edge_info': edge_info, 'start_node': start_node, 'n_nodes': self.n_nodes, 'n_edges': self.n_edges, # 'is_bifurcated_graph': self.is_bifurcated_graph, 'node_order': node_order, 'edge_order': edge_order } #print(json.dumps(ast.literal_eval(repr(tmp)),sort_keys=True,indent=2)) #sys.exit(0) #### Return the response return response