diff --git a/src/indra_cogex/apps/gla/gene_blueprint.py b/src/indra_cogex/apps/gla/gene_blueprint.py index 522ea289c..73e21bef6 100644 --- a/src/indra_cogex/apps/gla/gene_blueprint.py +++ b/src/indra_cogex/apps/gla/gene_blueprint.py @@ -232,6 +232,8 @@ def signed_analysis_route(): negative_genes=negative_genes, negative_errors=negative_errors, results=results, + minimum_evidence=form.minimum_evidence.data, + minimum_belief=form.minimum_belief.data ) return flask.render_template( "gene_analysis/signed_form.html", @@ -298,6 +300,9 @@ def continuous_analysis_route(): "gene_analysis/continuous_results.html", source=form.source.data, results=results, + gene_names=df[gene_name_column].values.tolist(), + minimum_evidence=form.minimum_evidence.data, + minimum_belief=form.minimum_belief.data, ) return flask.render_template( "gene_analysis/continuous_form.html", diff --git a/src/indra_cogex/apps/search/search.py b/src/indra_cogex/apps/search/search.py index 9da632b5f..eaa4cecad 100644 --- a/src/indra_cogex/apps/search/search.py +++ b/src/indra_cogex/apps/search/search.py @@ -1,5 +1,6 @@ import json from typing import List, Optional, Mapping, Tuple +import logging from flask import Blueprint, render_template, request, jsonify, redirect, url_for from flask_jwt_extended import jwt_required @@ -14,6 +15,8 @@ from indra_cogex.client.queries import * from indra_cogex.representation import norm_id +logger = logging.getLogger(__name__) + __all__ = ["search_blueprint"] from indra_cogex.client.queries import enrich_statements @@ -242,14 +245,14 @@ def get_ora_statements( # Enrich statements with complete evidence (no limit) stmts = enrich_statements( - stmts, - client=client + stmts, + client=client ) # Create evidence count mapping evidence_counts = { - stmt.get_hash(): rel.data.get("evidence_count", 0) - for rel, stmt in zip(flattened_rels, stmts) + stmt.get_hash(): rel.data.get("evidence_count", 0) + for rel, stmt in zip(flattened_rels, stmts) } return stmts, evidence_counts @@ -289,3 +292,291 @@ def search_ora_statements(): except Exception as e: print(f"Error in get_ora_statements: {str(e)}") return jsonify({"error": str(e)}), 500 + + +@autoclient() +def get_signed_statements( + target_id: str, + positive_genes: List[str], + negative_genes: List[str], + minimum_belief: float = 0.0, + minimum_evidence: Optional[int] = None, + *, + client: Neo4jClient, +) -> Tuple[List[Statement], Mapping[int, int]]: + """Get statements showing signed relationships between genes and target. + + Parameters + ---------- + target_id : str + The ID of the target entity + positive_genes : List[str] + List of gene IDs that show positive correlation with target + negative_genes : List[str] + List of gene IDs that show negative correlation with target + minimum_belief : float, optional + Minimum belief score threshold for relationships, by default 0.0 + minimum_evidence : Optional[int], optional + Minimum number of evidences required, by default None + client : Neo4jClient + The Neo4j client instance + + Returns + ------- + : + A tuple containing: + - List of INDRA statements representing the relationships + - Dictionary mapping statement hashes to evidence counts + """ + pos_genes = [norm_id('HGNC', gene.split(':')[1]) for gene in positive_genes] + neg_genes = [norm_id('HGNC', gene.split(':')[1]) for gene in negative_genes] + + namespace = target_id.split(':')[0].lower() + id_part = target_id.split(':')[1] + + if namespace == 'chebi': + normalized_target = f"chebi:{id_part}" + rel_types = ["indra_rel", "has_metabolite"] + elif namespace == 'mesh': + normalized_target = f"mesh:{id_part}" + rel_types = ["indra_rel", "has_indication"] + elif namespace == 'hgnc': + normalized_target = f"hgnc:{id_part}" + rel_types = ["indra_rel"] + elif namespace == 'fplx': + normalized_target = f"fplx:{id_part}" + rel_types = ["indra_rel", "isa"] + else: + normalized_target = target_id.lower() + rel_types = ["indra_rel"] + + query = """ + MATCH p = (gene:BioEntity)-[r]-(target:BioEntity) + WHERE target.id = $target_id + AND type(r) IN $rel_types + AND gene.id IN $gene_list + AND r.belief > $minimum_belief + AND r.evidence_count >= $minimum_evidence + AND NOT gene.obsolete + AND ( + (startNode(r) = gene AND r.stmt_type IN $stmt_types_gene_to_target) + OR + (startNode(r) = target AND r.stmt_type IN $stmt_types_target_to_gene) + ) + RETURN p + """ + + flattened_rels = [] + + if pos_genes: + pos_params = { + "target_id": normalized_target, + "gene_list": pos_genes, + "rel_types": rel_types, + "minimum_belief": minimum_belief, + "minimum_evidence": minimum_evidence, + "stmt_types_gene_to_target": [ + 'DecreaseAmount', + 'Inhibition' + ], + "stmt_types_target_to_gene": [ + 'IncreaseAmount', + 'Activation', + 'Complex' + ] + } + pos_results = client.query_tx(query, **pos_params) + for result in pos_results: + path = result[0] + rel = client.neo4j_to_relation(path) + flattened_rels.append(rel) + + if neg_genes: + neg_params = { + "target_id": normalized_target, + "gene_list": neg_genes, + "rel_types": rel_types, + "minimum_belief": minimum_belief, + "minimum_evidence": minimum_evidence, + "stmt_types_gene_to_target": [ + 'IncreaseAmount', + 'Activation' + ], + "stmt_types_target_to_gene": [ + 'DecreaseAmount', + 'Inhibition' + ] + } + neg_results = client.query_tx(query, **neg_params) + for result in neg_results: + path = result[0] + rel = client.neo4j_to_relation(path) + flattened_rels.append(rel) + + stmts = indra_stmts_from_relations(flattened_rels, deduplicate=True) + stmts = enrich_statements(stmts, client=client) + + evidence_counts = { + stmt.get_hash(): rel.data.get("evidence_count", 0) + for rel, stmt in zip(flattened_rels, stmts) + } + + return stmts, evidence_counts + + +@search_blueprint.route("/signed_statements/", methods=['GET']) +@jwt_required(optional=True) +def search_signed_statements(): + target_id = request.args.get("target_id") + positive_genes = request.args.getlist("positive_genes") + negative_genes = request.args.getlist("negative_genes") + minimum_evidence = request.args.get("minimum_evidence", "1") + minimum_belief = request.args.get("minimum_belief", "0.0") + + try: + minimum_evidence = int(minimum_evidence) + minimum_belief = float(minimum_belief) + except (ValueError, TypeError) as e: + return jsonify({"error": "Invalid parameter values"}), 400 + + if not target_id or (not positive_genes and not negative_genes): + return jsonify({"error": "target_id and at least one gene list required"}), 400 + + statements, evidence_counts = get_signed_statements( + target_id=target_id, + positive_genes=positive_genes, + negative_genes=negative_genes, + minimum_belief=minimum_belief, + minimum_evidence=minimum_evidence + ) + + return render_statements( + stmts=statements, + evidence_count=evidence_counts + ) + + +@autoclient() +def get_continuous_statements( + target_id: str, + gene_names: List[str], + source: str, + minimum_belief: float = 0.0, + minimum_evidence: Optional[int] = None, + *, + client: Neo4jClient, +) -> Tuple[List[Statement], Mapping[int, int]]: + """Get statements for continuous analysis. + + Parameters + ---------- + target_id : str + The ID of the target entity + gene_names : List[str] + List of gene names + source : str + Type of analysis ('indra-upstream' or 'indra-downstream') + minimum_belief : float + Minimum belief score for relationships + minimum_evidence : Optional[int] + Minimum number of evidences required + client : Neo4jClient + The Neo4j client to use for querying + Returns + ------- + : + A tuple containing: + - List of INDRA statements representing the relationships + - Dictionary mapping statement hashes to their evidence counts + """ + prefix, entity = target_id.split(':', 1) + if prefix.lower() in ['fplx', 'mesh']: + normalized_target = f"{prefix.lower()}:{entity}" + else: + normalized_target = target_id.lower() + + if source == 'indra-upstream': + query = """ + MATCH path=(gene:BioEntity)-[r:indra_rel]-(target:BioEntity) + WHERE target.id = $target_id + AND gene.name IN $gene_names + AND r.belief > $minimum_belief + AND r.evidence_count >= $minimum_evidence + RETURN path + """ + elif source == 'indra-downstream': + query = """ + MATCH path=(regulator:BioEntity)-[r:indra_rel]-(gene:BioEntity) + WHERE regulator.id = $target_id + AND gene.name IN $gene_names + AND r.belief > $minimum_belief + AND r.evidence_count >= $minimum_evidence + RETURN path + """ + + params = { + "target_id": normalized_target, + "gene_names": gene_names, + "minimum_belief": minimum_belief, + "minimum_evidence": minimum_evidence if minimum_evidence is not None else 0 + } + + results = client.query_tx(query, **params) + + all_relations = [] + for result in results: + try: + rels = client.neo4j_to_relations(result[0]) + all_relations.extend(rels) + except Exception: + continue + + if not all_relations: + return [], {} + + stmts = indra_stmts_from_relations(all_relations, deduplicate=True) + stmts = enrich_statements(stmts, client=client) + + evidence_counts = { + stmt.get_hash(): rel.data.get("evidence_count", 0) + for rel, stmt in zip(all_relations, stmts) + } + + return stmts, evidence_counts + + +@search_blueprint.route("/continuous_statements/", methods=['GET']) +@jwt_required(optional=True) +def search_continuous_statements(): + """Endpoint to get INDRA statements for continuous analysis results.""" + target_id = request.args.get("target_id") + genes = request.args.getlist("genes") + source = request.args.get("source", "indra-upstream") + + try: + minimum_evidence = int(request.args.get("minimum_evidence") or 1) + minimum_belief = float(request.args.get("minimum_belief") or 0.0) + except (ValueError, TypeError): + return jsonify({"error": "Invalid parameter values"}), 400 + + if not target_id or not genes: + return jsonify({"error": "target_id and genes are required"}), 400 + + try: + statements, evidence_counts = get_continuous_statements( + target_id=target_id, + gene_names=genes, + source=source, + minimum_belief=minimum_belief, + minimum_evidence=minimum_evidence + ) + + return render_statements( + stmts=statements, + evidence_count=evidence_counts + ) + + except ValueError as e: + return jsonify({"error": str(e)}), 400 + + diff --git a/src/indra_cogex/apps/templates/gene_analysis/continuous_results.html b/src/indra_cogex/apps/templates/gene_analysis/continuous_results.html index ac2da9f2d..b97dac784 100644 --- a/src/indra_cogex/apps/templates/gene_analysis/continuous_results.html +++ b/src/indra_cogex/apps/templates/gene_analysis/continuous_results.html @@ -52,7 +52,7 @@ {% endblock %} -{% macro render_table(df, table_id) -%} +{% macro render_table(results, table_id) -%} @@ -62,10 +62,11 @@ + - {% for curie, name, es, nes, p, q, geneset_size, matched_size in df.values %} + {% for curie, name, es, nes, p, q, geneset_size, matched_size in results.values %} @@ -73,6 +74,15 @@ + {% endfor %} diff --git a/src/indra_cogex/apps/templates/gene_analysis/signed_results.html b/src/indra_cogex/apps/templates/gene_analysis/signed_results.html index 540d7015d..af58c08e4 100644 --- a/src/indra_cogex/apps/templates/gene_analysis/signed_results.html +++ b/src/indra_cogex/apps/templates/gene_analysis/signed_results.html @@ -52,7 +52,7 @@ {% endblock %} -{% macro render_table(df, table_id) -%} +{% macro render_table(df, table_id, positive_genes, negative_genes, minimum_evidence, minimum_belief) -%}
NES p-value q-valueStatements
{{ curie }} {{ name }}{{ "{:.2f}".format(nes) }} {{ "{:.2e}".format(p) }} {{ "{:.2e}".format(q) }} + View Statements +
@@ -62,10 +62,19 @@ + {% for curie, name, correct, incorrect, ambig, p, conservative_p in df.values %} + {% set pos_gene_list = [] %} + {% set neg_gene_list = [] %} + {% for gene_id in positive_genes.keys() %} + {% do pos_gene_list.append("HGNC:" + gene_id) %} + {% endfor %} + {% for gene_id in negative_genes.keys() %} + {% do neg_gene_list.append("HGNC:" + gene_id) %} + {% endfor %} @@ -73,6 +82,16 @@ + {% endfor %} @@ -123,7 +142,7 @@

Reverse Causal Reasoning Analysis

query gene set. A p-value is calculated by applying a binomial test to the matches and non-matches.

- {{ render_table(results, "table-rcr") }} + {{ render_table(results, "table-rcr", positive_genes, negative_genes, minimum_evidence, minimum_belief) }}
🤷 p-valueStatements
{{ curie }} {{ name }}{{ incorrect }} {{ ambig }} {{ "{:.2e}".format(p) }} + View Statements +